#define _POSIX_C_SOURCE 200809L

#include "meow_hash_x64_aesni.h"

#define BUFFER_SIZE 8192
#define HASH_SIZE 128
#define MODE_SUM 0
#define MODE_CHECK 1

#include <assert.h>
#include <errno.h>
#include <fcntl.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>

void print_usage() {
  printf("usage: meowsum [OPTION]... FILE...\n");
  printf("Print or check Meow hash checksums.\n");
  printf("\n");
  printf("When FILE is -, read standard input.\n");
  printf("\n");
  printf("  -h           show this help and exit\n");
  printf("  -c           read sums from the FILEs and check them\n");
  printf("  -s size      set hash size; size can be 32, 64, 128 bits;\n");
  printf("               the default is 128\n");
  printf("\n");
  printf("The following options are useful only when verifying checksums:\n");
  printf("  -q           only return status; do not print OK, ERH, ERS, ERM\n");
  printf("  -e           fail for missing files\n");
}

void parse_args(int argc, char *argv[], bool *quiet, bool *strict_error,
                int *hash_size, int *first_file_index, int *mode, bool *help) {
  bool expecting_num = false;

  for (int i = 1; i < argc; ++i) {
    if (expecting_num) {
      *hash_size = atoi(argv[i]);
      expecting_num = false;
      continue;
    }
    if (argv[i][0] != '-' || strcmp("-", argv[i]) == 0) {
      *first_file_index = i;
      break;
    }
    if (strcmp(argv[i], "--") == 0) {
      *first_file_index = i + 1;
      break;
    }
    if (argv[i][0] == '-') {
      ssize_t length = strlen(argv[i]);
      for (int j = 1; j < length; ++j) {
        if (expecting_num) {
          *hash_size = atoi(argv[i] + j);
          expecting_num = false;
          break;
        }
        switch (argv[i][j]) {
        case 'c':
          *mode = MODE_CHECK;
          break;
        case 'q':
          *quiet = true;
          break;
        case 'e':
          *strict_error = true;
          break;
        case 's':
          expecting_num = true;
          break;
        case 'h':
          *help = true;
          break;
        }
      }
    }
  }
}

meow_u128 *calculate_checksum(char *path, unsigned long *size) {
  assert(size != NULL);
  assert(path != NULL);

  int fd;
  meow_state state;
  ssize_t read_bytes = 0;
  char buffer[BUFFER_SIZE];
  meow_u128 *hash = NULL;

  if (strcmp(path, "-") == 0) {
    fd = 0;
  } else {
    fd = open(path, 0);
  }
  if (fd == -1) {
    perror("error");
    fprintf(stderr, "error opening file %s\n", path);
    return NULL;
  }

  *size = 0;

  hash = malloc(sizeof(meow_u128));
  if (hash == NULL) {
    perror("error");
    fprintf(stderr, "error mallocing hash for %s\n", path);
    return NULL;
  }

  MeowBegin(&state, MeowDefaultSeed);
  while (true) {
    read_bytes = read(fd, buffer, 8192);
    if (read_bytes == -1) {
      perror("error");
      fprintf(stderr, "error reading file %s\n", path);
      close(fd);
      free(hash);
      return NULL;
    }
    *size += read_bytes;
    if (read_bytes == 0) {
      break;
    }
    MeowAbsorb(&state, read_bytes, buffer);
  }
  *hash = MeowEnd(&state, NULL);

  if (fd != 0) {
    close(fd);
  }

  return hash;
}

char *format_sum(meow_u128 hash, int size) {
  char *result;
  result = malloc((size * 2 / 8 * sizeof(char)) + 1);

  if (size == 32) {
    // NOLINTNEXTLINE
    sprintf(result, "%08x", MeowU32From(hash, 0));
  }
  if (size == 64) {
    // NOLINTNEXTLINE
    sprintf(result, "%016llx", MeowU64From(hash, 0));
  }
  if (size == 128) {
    int64_t v64val[2];
    // NOLINTNEXTLINE
    memcpy(v64val, &hash, sizeof(v64val));
    // NOLINTNEXTLINE
    sprintf(result, "%016lx%016lx", v64val[1], v64val[0]);
  }

  return result;
}

bool sum_files(char *argv[], int start, int argc, int hash_size) {
  meow_u128 *hash;
  char *hex_hash;
  unsigned long size;

  for (int i = start; i < argc; ++i) {
    hash = calculate_checksum(argv[i], &size);
    if (hash == NULL) {
      fprintf(stderr, "error while calculating %s\n", argv[i]);
      return false;
    }
    hex_hash = format_sum(*hash, hash_size);
    if (hex_hash == NULL) {
      fprintf(stderr, "error while formating hex hash for %s\n", argv[i]);
      free(hash);
      return false;
    }

    printf("%s %ld %s\n", hex_hash, size, argv[i]);
    free(hex_hash);
    free(hash);
  }

  return true;
}

bool read_sum_line(char **line, size_t *line_size, FILE *file, char *filename) {
  ssize_t read_bytes;

  read_bytes = getline(line, line_size, file);
  if (read_bytes == -1) {
    if (feof(file)) {
      *line = NULL;
      return true;
    } else {
      perror("error");
      fprintf(stderr, "while reading line in %s\n", filename);
      *line = NULL;
      return false;
    }
  }
  if (*line[read_bytes - 1] == '\n') {
    *line[read_bytes - 1] = 0;
  }
  return true;
}

bool parse_sum_line(char *line, unsigned int line_size,
                    char **expected_hex_hash, unsigned long *expected_size,
                    char **path, char *filename, unsigned int *hash_size) {
  char *expected_size_string;
  char *next;

  *expected_hex_hash = malloc(line_size * sizeof(char));
  if (expected_hex_hash == NULL) {
    perror("error");
    fprintf(stderr, "error mallocing expected_hex_hash for %s\n", filename);
    return false;
  }
  expected_size_string = malloc(line_size * sizeof(char));
  if (expected_size_string == NULL) {
    perror("error");
    fprintf(stderr, "error mallocing expected_size_string for %s\n", filename);
    free(*expected_hex_hash);
    return false;
  }
  *path = malloc(line_size * sizeof(char));
  if (path == NULL) {
    perror("error");
    fprintf(stderr, "error mallocing path for %s\n", filename);
    free(*expected_hex_hash);
    free(expected_size_string);
    return false;
  }
  int n =
      // NOLINTNEXTLINE
      sscanf(line, "%s %s %s", *expected_hex_hash, expected_size_string, *path);
  if (n != 3) {
    fprintf(stderr, "ERR line malformed\n");
    free(*expected_hex_hash);
    free(expected_size_string);
    free(*path);
    return false;
  }
  *hash_size = strlen(*expected_hex_hash) * 8 / 2;
  if (!(*hash_size == 32 || *hash_size == 64 || *hash_size == 128)) {
    fprintf(stderr, "ERR hash size is wrong\n");
    free(*expected_hex_hash);
    free(expected_size_string);
    free(*path);
    return false;
  }
  *expected_size = strtoul(expected_size_string, &next, 10);
  if (errno == ERANGE) {
    fprintf(stderr, "ERR expected size out of range\n");
    free(*expected_hex_hash);
    free(expected_size_string);
    free(*path);
    return false;
  } else if (next == expected_size_string) {
    fprintf(stderr, "ERR expected size is an invalid number\n");
    free(*expected_hex_hash);
    free(expected_size_string);
    free(*path);
    return false;
  } else if (*next != '\n' && *next != '\0') {
    fprintf(stderr, "ERR rubbish in expected size\n");
    free(*expected_hex_hash);
    free(expected_size_string);
    free(*path);
    return false;
  }
  free(expected_size_string);
  return true;
}

bool check_files(char *argv[], int start, int argc, bool quiet,
                 bool strict_errors) {
  char *line = NULL;
  size_t line_size = 0;
  FILE *file = NULL;
  char *expected_hex_hash = NULL, *path = NULL;
  unsigned long expected_size = 0, size = 0;
  unsigned int hash_size = 0;
  meow_u128 *hash = NULL;
  char *hex_hash = NULL;
  bool result = true;
  bool r;

  for (int i = start; i < argc; ++i) {
    file = fopen(argv[i], "r");

    while (true) {
      path = NULL;
      line = NULL;

      r = read_sum_line(&line, &line_size, file, argv[i]);
      if (line == NULL) {
        if (r) {
          break;
        } else {
          continue;
        }
      }
      r = parse_sum_line(line, line_size, &expected_hex_hash, &expected_size,
                         &path, argv[i], &hash_size);
      if (r == false) {
        continue;
      }

      if (strcmp("-", path) == 0) {
        if (!quiet) {
          printf("WARN stdin; skipping\n");
        }
      } else {
        if (access(path, F_OK) == -1) {
          if (!quiet) {
            printf("ERM %s\n", path);
          }
          if (strict_errors) {
            result &= false;
          }
          free(expected_hex_hash);
          free(path);
          free(line);
          continue;
        }
        hash = calculate_checksum(path, &size);
        if (hash == NULL) {
          fprintf(stderr, "ERR calculating %s\n", path);
          free(expected_hex_hash);
          free(path);
          free(line);
          continue;
        }
        hex_hash = format_sum(*hash, hash_size);
        if (hex_hash == NULL) {
          fprintf(stderr, "ERR formating hex hash %s\n", path);
          free(expected_hex_hash);
          free(hash);
          free(path);
          free(line);
          continue;
        }

        if (strcmp(expected_hex_hash, hex_hash) != 0) {
          if (!quiet) {
            printf("ERH %s\n", path);
          }
          result &= false;
        } else if (expected_size != size) {
          if (!quiet) {
            printf("ERS %s\n", path);
          }
          result &= false;
        } else {
          if (!quiet) {
            printf("OK %s\n", path);
          }
        }
        free(hex_hash);
        free(hash);
      }
      free(expected_hex_hash);
      free(path);
      free(line);
    }

    fclose(file);
  }
  return result;
}

int main(int argc, char *argv[]) {
  int hash_size = HASH_SIZE, mode = MODE_SUM, first_file_index = -1;
  bool quiet = false, strict_error = false, help = false;
  bool result;

  if (argc == 1) {
    print_usage();
    return 1;
  }

  parse_args(argc, argv, &quiet, &strict_error, &hash_size, &first_file_index,
             &mode, &help);

  if (first_file_index == -1 || help) {
    print_usage();
    return 1;
  }

  if (hash_size != 32 && hash_size != 64 && hash_size != 128) {
    fprintf(stderr, "unknown hash size %d\n", hash_size);
    return 1;
  }

  switch (mode) {
  case MODE_SUM:
    result = sum_files(argv, first_file_index, argc, hash_size);
    return result ? 0 : 1;
  case MODE_CHECK:
    result = check_files(argv, first_file_index, argc, quiet, strict_error);
    return result ? 0 : 1;
  default:
    print_usage();
    return 1;
  }
  return 0;
}
